package com.atguigu.gmall.list.service.impl;

import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.list.model.*;
import com.atguigu.gmall.list.repository.GoodsRepository;
import com.atguigu.gmall.list.service.SearchService;
import com.atguigu.gmall.product.client.ProductFeignClient;
import com.atguigu.gmall.product.model.BaseAttrInfo;
import com.atguigu.gmall.product.model.BaseCategoryView;
import com.atguigu.gmall.product.model.BaseTrademark;
import com.atguigu.gmall.product.model.SkuInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.NestedQueryBuilder;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
import org.elasticsearch.search.aggregations.bucket.nested.NestedAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;

/**
 * @author: atguigu
 * @create: 2023-04-26 14:17
 */
@Slf4j
@Service
@SuppressWarnings("all")
public class SearchServiceImpl implements SearchService {

    @Autowired
    private ProductFeignClient productFeignClient;

    @Autowired
    private GoodsRepository goodsRepository;

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    public static final String index_name = "goods";

    @Autowired
    private ThreadPoolExecutor threadPoolExecutor;

    @Autowired
    private RedisTemplate redisTemplate;



    /**
     * 将商品导入索引库
     *
     * @param skuId
     */
    @Override
    public void upperGoods(Long skuId) {
        try {
            //1.构建索引库文档对象Goods
            Goods goods = new Goods();
            goods.setId(skuId);
            //2.远程调用商品微服务-根据商品SkuID查询商品基本信息
            CompletableFuture<SkuInfo> skuInfoCompletableFuture = CompletableFuture.supplyAsync(() -> {
                SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId);
                if (skuInfo != null) {
                    goods.setCategory3Id(skuInfo.getCategory3Id());
                    goods.setDefaultImg(skuInfo.getSkuDefaultImg());
                    goods.setTitle(skuInfo.getSkuName());
                    goods.setTmId(skuInfo.getTmId());
                }
                return skuInfo;
            }, threadPoolExecutor);
            BigDecimal skuPrice = productFeignClient.getSkuPrice(skuId);
            if (skuPrice != null) {
                goods.setPrice(skuPrice.doubleValue());
            }
            //3.远程调用商品微服务-根据分类ID查询分类信息.
            CompletableFuture<Void> categoryCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
                BaseCategoryView categoryView = productFeignClient.getCategoryView(skuInfo.getCategory3Id());
                if (categoryView != null) {
                    goods.setCategory1Id(categoryView.getCategory1Id());
                    goods.setCategory1Name(categoryView.getCategory1Name());

                    goods.setCategory2Id(categoryView.getCategory2Id());
                    goods.setCategory2Name(categoryView.getCategory2Name());

                    goods.setCategory3Id(categoryView.getCategory3Id());
                    goods.setCategory3Name(categoryView.getCategory3Name());
                }
            }, threadPoolExecutor);

            //4.远程调用商品微服务-根据品牌ID查询品牌信息
            CompletableFuture<Void> priceCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
                BaseTrademark trademark = productFeignClient.getTrademark(skuInfo.getTmId());
                if (trademark != null) {
                    goods.setTmId(trademark.getId());
                    goods.setTmName(trademark.getTmName());
                    goods.setTmLogoUrl(trademark.getLogoUrl());
                }
            }, threadPoolExecutor);

            //5.远程调用商品微服务-根据skuID查询平台属性列表
            CompletableFuture<Void> attrInfoCompletableFuture = CompletableFuture.runAsync(() -> {
                List<BaseAttrInfo> attrList = productFeignClient.getAttrList(skuId);
                if (!CollectionUtils.isEmpty(attrList)) {
                    List<SearchAttr> searchAttrList = attrList.stream().map(baseAttrInfo -> {
                        SearchAttr searchAttr = new SearchAttr();
                        searchAttr.setAttrId(baseAttrInfo.getId());
                        searchAttr.setAttrName(baseAttrInfo.getAttrName());
                        searchAttr.setAttrValue(baseAttrInfo.getAttrValue());
                        return searchAttr;
                    }).collect(Collectors.toList());
                    goods.setAttrs(searchAttrList);
                }
            }, threadPoolExecutor);
            goods.setCreateTime(new Date());
            goods.setCreatedDate(new Date());

            CompletableFuture.allOf(
                    skuInfoCompletableFuture,
                    priceCompletableFuture,
                    attrInfoCompletableFuture,
                    categoryCompletableFuture).join();

            //6.调用JavaHighLevelRestClient完成文档新增  本质上发起http请求
            //6.1 构建创建文档请求对象 IndexReqeust  封装新增数据对应索引库库名称  文档ID
            IndexRequest request = new IndexRequest(index_name).id(skuId.toString());
            //6.2 构建新增文档请求体参数  JSON
            request.source(JSON.toJSONString(goods), XContentType.JSON);

            //6.3 发起请求 请求ES
            restHighLevelClient.index(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            log.error("[搜索服务]上架商品:{},失败原因:{}", skuId, e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 将指定商品从索引库删除
     *
     * @param skuId
     */
    @Override
    public void lowerGoods(Long skuId) {
        try {
            DeleteRequest request = new DeleteRequest(
                    index_name,
                    skuId.toString());
            restHighLevelClient.delete(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            log.error("[搜索服务]下架商品:{},失败原因:{}", skuId, e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 更新商品热度分值
     * 1.更新Redis缓冲中商品分值
     * 2.更新ES文档中商品分值(稀释写操作)
     *
     * @param skuId
     */
    @Override
    public void incrHotScore(Long skuId) {
        try {
            // 1.更新Redis缓冲中商品分值
            String key = "hotScore";
            Double skuHotScore = redisTemplate.opsForZSet().incrementScore(key, skuId.toString(), 1);
            // 2.更新ES文档中商品分值(稀释写操作)
            if (skuHotScore % 10 == 0) {
                //2.1 创建更新文档请求对象 文档ID 文档分值
                UpdateRequest updateRequest = new UpdateRequest(index_name, skuId.toString());
                Goods goods = new Goods();
                goods.setHotScore(skuHotScore.longValue());
                updateRequest.doc(JSON.toJSONString(goods), XContentType.JSON);
                restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
            }
        } catch (IOException e) {
            log.error("[搜索微服务]更新商品分值:{},异常信息:{}", skuId, e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 完成根据条件检索ES索引库得到业务数据跟过滤条件
     *
     * @param searchParam 检索条件
     * @return
     */
    @Override
    public SearchResponseVo search(SearchParam searchParam) {
        try {
            //一.构建搜索请求对象-(封装请求方式,路径,请求体参数)
            SearchRequest searchRequest = this.builderDSL(searchParam);

            //二.执行检索
            SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

            //三.封装响应结果
            return this.parseResult(response, searchParam);
        } catch (Exception e) {
            log.error("[搜索服务]检索异常:{},条件:{}", e, searchParam);
            throw new RuntimeException(e);
        }
    }


    /**
     * 构建查询请求对象,构建DSL语句中请求跟请求参数
     *
     * @param searchParam
     * @return
     */
    private SearchRequest builderDSL(SearchParam searchParam) {
        //1.构建搜索请求对象 封装 GET /goods/_search 请求方式;请求路径参数中索引库名称
        SearchRequest searchRequest = new SearchRequest(index_name);

        //2.构建请求体构建器对象 封装请求体JSON参数 例如:查询;分页;高亮;排序;字段指定;聚合
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        //2.1 设置查询条件,请求参数中"query"部分
        //2.1.1 构建所有封装布尔查询条件对象
        BoolQueryBuilder allBoolQueryBuilder = QueryBuilders.boolQuery();
        //2.1.2 根据关键字检索-采用匹配/模糊/关键字 查询
        if (StringUtils.isNotBlank(searchParam.getKeyword())) {
            allBoolQueryBuilder.must(QueryBuilders.matchQuery("title", searchParam.getKeyword()).operator(Operator.AND));
        }
        //2.1.3 设置布尔查询条件中过滤 1.品牌过滤 2.平台属性过滤
        //2.1.3.1 设置品牌过滤
        if (StringUtils.isNotBlank(searchParam.getTrademark())) {

            String[] split = searchParam.getTrademark().split(":");
            if (split != null && split.length == 2) {
                allBoolQueryBuilder.filter(QueryBuilders.termQuery("tmId", split[0]));
            }
        }
        //2.1.3.1 设置分类过滤
        if (searchParam.getCategory1Id() != null) {
            allBoolQueryBuilder.filter(QueryBuilders.termQuery("category1Id", searchParam.getCategory1Id()));
        }
        if (searchParam.getCategory2Id() != null) {
            allBoolQueryBuilder.filter(QueryBuilders.termQuery("category2Id", searchParam.getCategory2Id()));
        }
        if (searchParam.getCategory3Id() != null) {
            allBoolQueryBuilder.filter(QueryBuilders.termQuery("category3Id", searchParam.getCategory3Id()));
        }
        //2.1.3.3 设置平台属性过滤
        //构建封装所有平台属性布尔查询
        BoolQueryBuilder allAttrBoolQueryBuilder = QueryBuilders.boolQuery();
        if (searchParam.getProps() != null && searchParam.getProps().length > 0) {
            //动态构建某个平台属性过滤条件
            for (String prop : searchParam.getProps()) {
                String[] split = prop.split(":");
                if (split != null && split.length == 3) {
                    // 每循环一次构建一个nested查询 每一组平台属性过滤条件 又是一个布尔查询
                    BoolQueryBuilder attrBoolQueryBuilder = QueryBuilders.boolQuery();
                    attrBoolQueryBuilder.must(QueryBuilders.termQuery("attrs.attrId", split[0]));
                    attrBoolQueryBuilder.must(QueryBuilders.termQuery("attrs.attrValue", split[1]));
                    NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", attrBoolQueryBuilder, ScoreMode.None);
                    allAttrBoolQueryBuilder.must(nestedQuery);
                }
            }
        }
        allBoolQueryBuilder.filter(allAttrBoolQueryBuilder);

        sourceBuilder.query(allBoolQueryBuilder);
        //2.2 设置高亮,请求参数中"highlight"部分 高亮前提:用户采用关键字检索
        if (StringUtils.isNotBlank(searchParam.getKeyword())) {
            HighlightBuilder highlightBuilder = new HighlightBuilder();
            //2.2.1 设置前置标签
            highlightBuilder.preTags("<font style='color:red'>");
            //2.2.2 设置后置标签
            highlightBuilder.postTags("</font>");
            //2.2.3 设置高亮字段
            highlightBuilder.field("title");
            sourceBuilder.highlighter(highlightBuilder);
        }
        //2.3 设置分页,请求参数 "from" "size"
        //2.3.1 计算起始位置索引 (pageNo-1)*pageSize
        int from = (searchParam.getPageNo() - 1) * searchParam.getPageSize();
        sourceBuilder.from(from).size(searchParam.getPageSize());
        //2.4 设置排序,请求参数 "sort" 前提:前端提交排序条件 参数形式: 2:asc  1:按照热度排 2:价格
        if (StringUtils.isNotBlank(searchParam.getOrder())) {
            String[] split = searchParam.getOrder().split(":");
            if (split != null && split.length == 2) {
                String orderField = "";
                if (split[0].equals("1")) {
                    //按照热度排名
                    orderField = "hotScore";
                } else if (split[0].equals("2")) {
                    //按照价格排名
                    orderField = "price";
                }
                sourceBuilder.sort(orderField, "asc".equals(split[1]) ? SortOrder.ASC : SortOrder.DESC);
            }

        }
        //2.5 设置响应文档字段列表,请求参数 "_source"
        sourceBuilder.fetchSource(new String[]{"id", "title", "price", "defaultImg"}, null);
        //2.6 设置聚合,请求参数 "aggs"
        //2.6.1 设置品牌聚合
        //2.6.1.1 先创建品牌ID聚合对象 terms("聚合名称")
        TermsAggregationBuilder tmIdAggregation = AggregationBuilders.terms("tmIdAgg").field("tmId");
        //2.6.1.1 基于品牌ID聚合对象设置子聚合-品牌名称聚合
        tmIdAggregation.subAggregation(AggregationBuilders.terms("tmNameAgg").field("tmName"));
        //2.6.1.2 基于品牌ID聚合对象设置子聚合-品牌Logo聚合
        tmIdAggregation.subAggregation(AggregationBuilders.terms("tmLogoAgg").field("tmLogoUrl"));
        sourceBuilder.aggregation(tmIdAggregation);

        //2.6.2 设置平台属性聚合
        //2.6.2.1 创建嵌套类型nested平台属性聚合对象
        NestedAggregationBuilder nestedAggregation = AggregationBuilders.nested("attrAgg", "attrs");
        //2.6.2.2 创建平台属性ID聚合对象
        TermsAggregationBuilder attrIdAgg = AggregationBuilders.terms("attrIdAgg").field("attrs.attrId");
        //2.6.2.2.1 基于平台属性ID聚合对象创建平台属性名称聚合对象
        attrIdAgg.subAggregation(AggregationBuilders.terms("attrNameAgg").field("attrs.attrName"));
        //2.6.2.2.2 基于平台属性ID聚合对象创建平台属性值聚合对象
        attrIdAgg.subAggregation(AggregationBuilders.terms("attrValueAgg").field("attrs.attrValue"));

        //2.6.2.3 将平台属性ID聚合对象加入到平台属性聚合对象中
        nestedAggregation.subAggregation(attrIdAgg);
        sourceBuilder.aggregation(nestedAggregation);

        //3.在搜索请求对象中添加请求体构建器对象
        return searchRequest.source(sourceBuilder);
    }


    /**
     * 从ES封装响应对象中获取所需要的数据封装为自定义VO对象
     *
     * @param response    ES响应结果JSON对象
     * @param searchParam 用户查询过滤条件
     * @return
     */
    private SearchResponseVo parseResult(SearchResponse response, SearchParam searchParam) {
        //构建响应Vo对象
        SearchResponseVo searchResponseVo = new SearchResponseVo();
        //1.封装响应Vo对象中分页信息
        searchResponseVo.setPageNo(searchParam.getPageNo());
        Integer pageSize = searchParam.getPageSize();
        searchResponseVo.setPageSize(pageSize);
        long total = response.getHits().getTotalHits().value;
        searchResponseVo.setTotal(total);
        Long totalPages = total % pageSize == 0 ? (total / pageSize) : (total / pageSize + 1);
        searchResponseVo.setTotalPages(totalPages);

        //2.封装响应Vo对象中当前页业务数据
        SearchHit[] hits = response.getHits().getHits();
        if (hits != null && hits.length > 0) {
            //遍历命中数据得到业务JSON-将JSON反序列化为Java对象Goods
            List<SearchHit> searchHits = Arrays.asList(hits);
            List<Goods> goodsList = searchHits.stream().map(hit -> {
                //获取商品文档JSON数据
                String goodsJsonStr = hit.getSourceAsString();
                //转为Goods对象
                Goods goods = JSON.parseObject(goodsJsonStr, Goods.class);
                //获取高亮结果
                Map<String, HighlightField> highlightFields = hit.getHighlightFields();
                if (!CollectionUtils.isEmpty(highlightFields)) {
                    //获取标题高亮结果对象
                    HighlightField highlightField = highlightFields.get("title");
                    if (highlightField != null) {
                        //获取高亮片段数组
                        Text[] fragments = highlightField.getFragments();
                        if (fragments != null && fragments.length > 0) {
                            //获取高亮内容
                            String titleHightLight = fragments[0].toString();
                            goods.setTitle(titleHightLight);
                        }
                    }
                }
                return goods;
            }).collect(Collectors.toList());
            searchResponseVo.setGoodsList(goodsList);
        }

        //3.封装响应Vo对象中用于展示过滤项-品牌聚合结果
        //3.1 获取到所有的聚合结果-Map
        Map<String, Aggregation> allAggregationMap = response.getAggregations().asMap();
        //3.2 获取品牌ID聚合结果对象 将聚合结果Aggregation接口转为实现类 ParsedXXTerms
        ParsedLongTerms tmIdAgg = (ParsedLongTerms) allAggregationMap.get("tmIdAgg");
        //3.2.1 从tmId聚合对象中获取bucket数组
        List<? extends Terms.Bucket> buckets = tmIdAgg.getBuckets();
        if (!CollectionUtils.isEmpty(buckets)) {
            List<SearchResponseTmVo> responseTmVos = buckets.stream().map(bucket -> {
                SearchResponseTmVo searchResponseTmVo = new SearchResponseTmVo();
                //遍历bucket数组获取品牌bucket对象
                //获取到聚合到品牌ID
                long tmId = bucket.getKeyAsNumber().longValue();
                //3.2.2 基于品牌ID聚合结果对象获取子聚合对象-品牌名称
                //3.2.3 基于品牌ID聚合结果对象获取子聚合对象-品牌Logo
                Map<String, Aggregation> tmNameAndLogoAggregation = bucket.getAggregations().asMap();
                if (!CollectionUtils.isEmpty(tmNameAndLogoAggregation)) {
                    //获取到品牌名称
                    ParsedStringTerms tmNameAgg = (ParsedStringTerms) tmNameAndLogoAggregation.get("tmNameAgg");
                    String tmName = tmNameAgg.getBuckets().get(0).getKeyAsString();
                    ParsedStringTerms tmLogoAgg = (ParsedStringTerms) tmNameAndLogoAggregation.get("tmLogoAgg");
                    String tmLogoUrl = tmLogoAgg.getBuckets().get(0).getKeyAsString();
                    searchResponseTmVo.setTmName(tmName);
                    searchResponseTmVo.setTmLogoUrl(tmLogoUrl);
                }
                searchResponseTmVo.setTmId(tmId);
                return searchResponseTmVo;
            }).collect(Collectors.toList());
            searchResponseVo.setTrademarkList(responseTmVos);
        }

        //4.封装响应Vo对象中用于展示过滤项-平台属性聚合结果
        //4.1 获取平台属性(nested)聚合对象
        ParsedNested attrAgg = (ParsedNested) allAggregationMap.get("attrAgg");
        //4.2 基于平台属性聚合对象 获取 平台属性ID子聚合对象
        ParsedLongTerms attrIdAgg = attrAgg.getAggregations().get("attrIdAgg");
        //4.3 通过平台属性聚合对象 获取 聚合结果bucket数组 遍历bucket数组-获取平台属性,以及名称,属性值
        List<? extends Terms.Bucket> attrIdAggBuckets = attrIdAgg.getBuckets();
        if (!CollectionUtils.isEmpty(attrIdAggBuckets)) {
            List<SearchResponseAttrVo> attrVoList = attrIdAggBuckets.stream().map(bucket -> {
                SearchResponseAttrVo attrVo = new SearchResponseAttrVo();
                //4.3.1 通过bucket对象获取聚合到平台属性ID
                long attrId = bucket.getKeyAsNumber().longValue();
                attrVo.setAttrId(attrId);
                //4.3.2 通过bucket对象获取平台属性名称子聚合对象
                ParsedStringTerms attrNameAgg = bucket.getAggregations().get("attrNameAgg");
                if (!CollectionUtils.isEmpty(attrNameAgg.getBuckets())) {
                    String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();
                    attrVo.setAttrName(attrName);
                }
                //4.3.3 通过bucket对象获取平台属性值子聚合对象
                ParsedStringTerms attrValueAgg = bucket.getAggregations().get("attrValueAgg");
                List<? extends Terms.Bucket> valueAggBuckets = attrValueAgg.getBuckets();
                if (!CollectionUtils.isEmpty(valueAggBuckets)) {
                    //直接映射bucket集合,获取每个桶中key转为字符串
                    List<String> attrValueList = valueAggBuckets
                            .stream().map(MultiBucketsAggregation.Bucket::getKeyAsString)
                            .collect(Collectors.toList());
                    attrVo.setAttrValueList(attrValueList);
                }
                return attrVo;
            }).collect(Collectors.toList());
            searchResponseVo.setAttrsList(attrVoList);
        }
        return searchResponseVo;
    }
}
